出处:掘金

原作者:ErpanOmer


引言

如果你曾在浏览器中使用 Web Worker 或 Node.js 的 Worker Thread 编写多线程代码,那么你一定遇到过这两个概念:

它们的名字看起来只差一个“Shared”,实则背后承载的是线程安全模型内存隔离策略,甚至涉及到了浏览器的安全沙箱机制

本篇文章将带你深入探索:

背景知识:JS 世界本来是单线程的

JavaScript 的运行模型是单线程的,意味着同一时间只能执行一段代码。为了避免 UI 卡顿,HTML5 引入了 Web Worker(浏览器)和 Worker Thread(Node.js),用来并行执行任务

问题是,多线程就需要解决一个问题:数据怎么传?

postMessage() 是通过结构化克隆算法来传输数据的:

ArrayBuffer:传得了,看不到

ArrayBuffer

ArrayBuffer 是 JavaScript 操作二进制数据的底层数据结构。它提供了一段连续的、固定长度的原始内存,用于存储数据

const buffer = new ArrayBuffer(8); // 8 字节
const view = new Uint8Array(buffer);
view[0] = 42;

特点:

worker.postMessage(buffer, [buffer]); // Transfer

这个“可转移对象机制”是一种内存拥有权的转移,不是共享。发送方再想访问?对不起,byteLength = 0

所以 ArrayBuffer 是一种“可搬家,但不共居”的模型

SharedArrayBuffer:共享内存的特洛伊木马

SharedArrayBuffer 的出现正是为了解决上述问题:在多个线程间共享一块内存,同时保持数据一致性

const shared = new SharedArrayBuffer(8);
const view = new Int32Array(shared);

view[0] = 123;

可以把它传给多个 Worker,无需复制,大家直接指向同一块物理内存

worker.postMessage(shared); // 不需要 transfer 列表

关键差异

特性 ArrayBuffer SharedArrayBuffer
是否共享 否(复制/转移) 是(直接引用)
是否可转移 否(共享无须转移)
可否在多个线程访问 是(并发访问)
是否线程安全 否(需配合 Atomics)
是否可直接修改

注意:SharedArrayBuffer 本身并不保证线程安全,你仍需使用 Atomics 提供的原子操作

内存隔离机制详解

ArrayBuffer 的隔离策略

当你通过 postMessage() 传递 ArrayBuffer

这是浏览器为 JS 实现的“堆隔离”策略 —— 避免线程交叉污染

SharedArrayBuffer 的共享实现

SharedArrayBuffer 本质上由浏览器的底层内存模型支持:

更关键的是,它跳过了结构化克隆的隔离机制,完全依赖开发者自己控制一致性

为什么浏览器曾禁用 SharedArrayBuffer?

一个关键词:Spectre

Spectre 是一种处理器层面的旁路攻击漏洞,攻击者可以利用预测执行 + 缓存时序分析,越权读取别的线程的内存数据

SharedArrayBuffer 刚好是“极佳入口”:

于是浏览器在 2018 年紧急禁用了它,直到提供了对应的安全缓解机制:

安全防护机制:COOP + COEP

配置如下 HTTP Header 才能启用 SharedArrayBuffer

Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp

你可以在浏览器控制台看到报错提示,不配这个头,SharedArrayBuffer 根本不能用

原子性保障:Atomics 是你的好兄弟

SharedArrayBuffer 让线程能访问同一内存,但会有竞争条件(Race Condition)

举例:

// 两个线程同时执行
view[0] += 1;

可能在底层变成:

解决办法是使用 Atomics

Atomics.add(view, 0, 1); // 原子加 1

它保证了内存操作的原子性,底层依赖 CPU 的原子指令(如 x86 的 LOCK 前缀)

你还能用:

这是 JavaScript 走向并发编程的重要一步

实际案例:线程安全的计数器

主线程:

const shared = new SharedArrayBuffer(4); // 4 字节 = 1 个 Int32
const counter = new Int32Array(shared);

const worker = new Worker('./worker.js');
worker.postMessage(shared);

worker.js:

onmessage = function(e) {
  const counter = new Int32Array(e.data);

  for (let i = 0; i < 10000; i++) {
    Atomics.add(counter, 0, 1);
  }

  postMessage('done');
}

这个方式可以安全地在多个线程对同一个变量进行累加操作,结果是稳定的、不会丢失

未来展望:结构共享的新时代?

SharedArrayBufferAtomics 已经是 JS 并发编程的重要组成部分。未来的方向包括:

但与此同时,安全机制始终是底线,未来浏览器将持续强化隔离能力和 CSP 限制

总结